9 Java Generics
Con le Java Generics è possibile implementare il concetto di tipo parametrizzato, che permette di creare componenti utilizzabili con più tipi.
Le Java Generics:
Permettono di specificare il tipo degli oggetti contenuti
Demandano al compilatore la possibilità di effettuare controlli di tipo
Tutte le classi contenitore sono definite come classi generiche.
Le Java Generics possono essere anche utilizzate per parametrizzare la dichiarazione di interfacce.
public class Holder<T> {
private T a;
public Holder(T a) {
this.a = a;
}
public void set(T a) {
this.a = a;
}
public T get() {
return a;
}
public static void main(String[] args) {
// ❗ Holder h2; dichiara un Holder di tipo Object
Holder<Automobile> h3 = new Holder<>(new Automobile());
= h3.get(); // ❗ No cast needed
Automobile a .set("Not an Automobile"); // ❗ Errore di compilazione
h3.set(1); // ❗ Errore di compilazione
h3Holder<String> h4 = new Holder<>("Not an Automobile");
}
}
Oltre a parametrizzare la dichiarazione di intere classi, è possibile parametrizzare la dichiarazione di metodi all’interno di una classe.
Un metodo può essere definito generico indipendentemente dal fatto che la classe sia generica oppure no.
Per di più, se un metodo definito in una classe parametrizzata è statico, tale metodo non accederà al parametro di tipo della classe perchè è il compilatore che crea i legami per gli static method e non vengono fatti a runtime.
9.1 Metodi generici
Per definire un metodo come generico è sufficiente parametrizzare la sua dichiarazione.
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
= new GenericMethods();
GenericMethods gm .f("");
gm.f(1);
gm.f(1.0);
gm}
}
/*
Output:
java.lang.String
java.lang.Integer
java.lang.Double
*/
9.2 Erasure
Le Java Generics lasciano comunque alcune questioni poco chiare. Per esempio, mentre è possibile ricorrere al letterale di classe per la classe ArrayList, non è possibile ricorrere al letterale di classe per la classe ArrayList ottenuta parametrizzando il tipo del contenuto.
Consideriamo il seguente esempio:
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}
:
Outputtrue
Il compilatore ha creato l’object class e ha compilato il codice generico, ovvero non ha l’informazione su come il T verrà istanziato. Stessa cosa se interroghiamo i metodi della classe per farci restituire i parametri, avremo T.
Le generics sono implementate usando l’erasure (cancellazione). Questo significa che ogni informazione di tipo è cancellata quando si ricorre all’astrazione generica. Così ArrayList<String>
e ArrayList<Integer>
sono, di fatto, lo stesso tipo al run-time.
Il costo dell’erasure è significante. Tipi generici non possono essere usati in operazioni che si riferiscono a tipi inferiti a runtime, come cast, instanceof oppure new.
In sostanza se ho il seguente pezzo di codice:
class Foo<T> {
var;
T }
Quando creo una istanza di Foo:
<Cat> f = new Foo<Cat>(); Foo
La classe Foo non sa che ora sta lavorando con un Cat, cioè dove nella classe vi era T questo viene sostituito dal compilatore con Object.
Un modo che abbiamo per limitare questo comportamento è definire dei “confini”:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
Scriviamo un metodo draw generico che vogliamo però che venga applicato a tipi di classi che estendono Shape, quindi nel nostro caso solo a Circle e Rectangle
public <T extends Shape> void draw(T shape) { /* ... */ }
Il compilatore rimpiazzerà T con Shape:
public void draw(Shape shape) { /* ... */ }
````